package org.jpropelleralt.jview.impl;

import java.awt.BorderLayout;
import java.util.HashMap;
import java.util.Map;

import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

import org.jpropelleralt.box.Box;
import org.jpropelleralt.change.Change;
import org.jpropelleralt.jview.JView;
import org.jpropelleralt.jview.factory.JViewFactory;
import org.jpropelleralt.jview.node.impl.NodeView;
import org.jpropelleralt.jview.ref.impl.GeneralView;
import org.jpropelleralt.node.Node;
import org.jpropelleralt.ref.Ref;
import org.jpropelleralt.ref.impl.ClassFilterRef;
import org.jpropelleralt.universe.Universe;
import org.jpropelleralt.view.update.UpdateManager;

/**
 * A {@link FlexibleView} will view any class for which a {@link JView} is
 * available from a {@link JViewFactory}, and may also be set to fall back to
 * using a {@link NodeView} for {@link Node}s without a specific {@link JView}.
 * Also has fallback views for when value is null, and a default view for 
 * when no more specific view is available.
 * <p/>
 * {@link JView}s are cached for performance, and so {@link FlexibleView} is
 * best used in cases where there are a relatively small number of viewed
 * classes.
 */
public class FlexibleView implements JView {

	private JPanel panel;

	/**
	 * Store the views we have used in the past - we use these in preference to
	 * new ones
	 */
	private Map<Class<?>, JView> views = new HashMap<Class<?>, JView>();

	private GeneralView generalView;
	private NodeView nodeView;
	private Ref<?> ref;
	private final Ref<Boolean> locked;
	private final Ref<String> name;
	private final Universe universe;
	
	/**
	 * The currently-used view
	 */
	private JView currentView;

	private UpdateManager updateManager;

	private final JViewFactory viewFactory;
	private final JView blankView;
	private final JView defaultView;
	private final JComponent busyComponent;
	
	private boolean displayNodeView = false;

	/**
	 * Make a {@link FlexibleView}
	 * 
	 * @param ref					The {@link Ref} to view
	 * @param viewFactory			The {@link JViewFactory} to use to look up views for classes
	 * @param displayNodeView		True to fallback to a {@link NodeView} for {@link Node}s rather than the
	 *            					blankView.
	 * @param blankView				View to display when contents are null.
	 * @param defaultView			View to display when no more specific view is available. Should probably display
	 * 								contents of ref in some way, but doesn't actually have to.
	 * @param busyComponent			Component to show while we are waiting to generate a new one, or null to leave display
	 * 								on old view while switching.
	 * @param updateManager			The {@link UpdateManager}
	 * @param locked				While this is true, the view will not edit values
	 * @param name					The name for contained {@link JView}s
	 * @param universe				The {@link Universe} that will be used to create {@link Ref}s used
	 * 								by this view,
	 */
	public FlexibleView(Ref<?> ref, JViewFactory viewFactory,
			final boolean displayNodeView, 
			final JView blankView,
			final JView defaultView,
			final JComponent busyComponent,
			UpdateManager updateManager, 
			Ref<Boolean> locked, Ref<String> name,
			Universe universe) {
		this.ref = ref;
		this.viewFactory = viewFactory;
		this.locked = locked;
		this.name = name;
		this.universe = universe;
		this.busyComponent = busyComponent;
		
		panel = new JPanel(new BorderLayout());

		// Initial general view
		generalView = GeneralView.create(ref);
		currentView = generalView;
		changeToView(generalView);
		
		this.displayNodeView = displayNodeView;

		this.blankView = blankView;
		this.defaultView = defaultView;

		this.updateManager = updateManager;
		updateManager.registerUpdatable(this);

		// Listen to the reference
		ref.features().addView(this);

		// Initial update
		update();
	}

	@Override
	public void changes(Map<Box, Change> changes) {
		// We only require an update on a shallow change to ref - that is,
		// when a new value of ref is set, NOT when one of the props of ref
		// changes. When a deep change occurred, it will be displayed by a
		// sub view update

		// First see if the ref value has changed at all
		if (changes.containsKey(ref)) {
			// Only update if the ref has a change to any instances
			if (changes.get(ref).shallow()) {
				updateManager.updateRequiredBy(this);
			}
		}
	}

	@Override
	public void update() {

		//If we have a busy component, change to it while we wait for the correct new 
		//view
		changeToBusy();

		// Views may change props, so we need to do this later, outside
		// change response.
		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				// We only update when we may have a new ref value - when this
				// is the case,
				// we need to check whether we need a new editor.
				JView newView = findView();

				// New value may be same (or compatible) class
				if (currentView == newView) {
					return;
				}

				changeToView(newView);
			}
		});
	}

	private void changeToComponent(JComponent newComponent) {
		panel.removeAll();

		panel.add(newComponent);
		panel.revalidate();
		panel.repaint();
	}

	
	private void changeToView(JView newView) {
		changeToComponent(newView.getComponent());
		currentView = newView;
	}

	private void changeToBusy() {
		if (busyComponent != null) {
			changeToComponent(busyComponent);
			currentView = null;
		}
	}

	//Safe for reasons discussed below
	@SuppressWarnings({ "rawtypes", "unchecked" })
	private JView createView(Class clazz) {
		
		//Make a ref that will filter our main ref, ensuring that
		//only values matching the specified class are seen. Note that
		//this is type safe by design, since we know that clazz is
		//the actual class of a value of the ref.
		ClassFilterRef filterRef = ClassFilterRef.createUnsafe(universe, clazz, ref);
		
		//Again, this seems not to be typesafe, but the clazz parametric type
		//definitely matches the filterRef parametric type.
		JView newView = viewFactory.viewFor(clazz, filterRef, locked, name);
		
		//If we've failed to find a view, and the clazz is a bean, and we
		//are set to display node view, make the node view so we can use that later
		if (newView == null && displayNodeView && Node.class.isAssignableFrom(clazz) && nodeView == null) {
			nodeView = NodeView.create(filterRef);
		}
		
		return newView;
	}

	private JView findView() {
		Object value = ref.get();

		// Just use blank view for nulls
		if (value == null) {
			return blankView;
		}

		// Otherwise look up from class
		Class<?> clazz = value.getClass();

		//If we have a view already in map, use it
		if (views.containsKey(clazz)) {
			return views.get(clazz);
			
		//Lazily try to make a view for this class,
		//if we succeed, use it
		} else {
			JView newView = createView(clazz);
			if (newView != null) {
				views.put(clazz, newView);
				return newView;
			}
		}

		//Try to fall back to a NodeView
		if (nodeView != null && Node.class.isAssignableFrom(clazz)) {
			return nodeView;
		}
		
		// We failed to get anything else, use default view
		return defaultView;
	}

	@Override
	public JComponent getComponent() {
		return panel;
	}

	@Override
	public boolean selfNaming() {
		// Just displays editor, not in general self-naming
		return false;
	}

	@Override
	public Format format() {
		// May fall back to using a bean editor, and so it is safest for this to
		// be "Large"
		return Format.LARGE;
	}

}
